﻿/* TODO
 * 
 * extrahovat parser do samostatne classy
 * refactoring parseru (cast vnitrni funkcionality by mohla byt v samostatne tride)
 * predelat ParserException na dva druhy vyjimek (uzivatelska a programatorska)
 * komentare, finalni refactoring, dokumentace
 * 
 */

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.Xml;
using System.ComponentModel;
using dpptask2.OptionClasses;
using System.Reflection;
using dpptask2.Exceptions;

namespace dpptask2
{
#pragma warning restore 649
    public class Parser
    {
        
        private string[] _ordinaryParameters;

        public string[] ordinaryParameters
        {
            get { return _ordinaryParameters; }
        }

        public static void printUsage(object args)
        {
            if (args == null)
                return;
            
            Type type = args.GetType();
            var fields = type.GetFields();
            var optionedFields = getOptionedFields(fields);

            Console.WriteLine("Usage:\n");

            string formatString = " {0, -30} {1}\n";

            foreach (var field in optionedFields)
            {
                optionAttribute at = field.Value;
                Console.WriteLine(formatString, at.getUsageText(at.command), at.description);

                string altInfo = "The same as " + at.command;

                if (at.alternatives != null)
                {
                    foreach (string alternativeCmd in at.alternatives)
                    {
                        Console.WriteLine(formatString, at.getUsageText(alternativeCmd), altInfo);
                    }
                }
            }

        }

        private  static IEnumerable<KeyValuePair<FieldInfo, optionAttribute>> getOptionedFields (FieldInfo[] fields)
        {
            // vytvori seznam dvojic field - optionAttribute (vcetne null u tech fields, ktere nejsou
            // oznaceny [optionAttribute]

            Dictionary<FieldInfo, optionAttribute> pairList =
                (from field in fields
                 select new KeyValuePair<FieldInfo, optionAttribute>(field, getOptionAttribute(field)))
                 .ToDictionary(pair => pair.Key, pair => pair.Value);

            // tady si vybereme ty dvojice field-option, ktere jsou oznaceny [optionAttribute]
            var optionedFields = from pair in pairList
                                     where pair.Value != null
                                     select pair;

            return optionedFields;
        }

        // tokenparser
        private List<CmdLineToken> createTokenList(string[] commands)
        {
            bool separatorFound = false;
            List<CmdLineToken> tokens = new List<CmdLineToken>();

            foreach (string s in commands)
            {

                CmdLineToken t = new CmdLineToken();

                t.isChoice = (!separatorFound && (s.StartsWith(ParserConsts.CHOICESTARTSHORT) || s.StartsWith(ParserConsts.CHOICESTARTLONG)));
                t.isUsed = false;
                t.originalText = s;
                t.nonSlashedText = removeBeginSlash(s);

                tokens.Add(t);

                if (s.Equals(ParserConsts.ARGUMENT_SEPARATOR))
                {
                    separatorFound = true;
                    t.isChoice = false;
                    t.isUsed = true;
                    separatorFound = true;
                }
            }

            return tokens;
        }

        // tokenparser
        private List<string> UsedCommands = new List<string>();

        // tokenparser
        private void addToUsedCommands(string s)
        {
            if (UsedCommands.Contains(s))
                throw new ParserException("Choice {0} already used.", s);
            else
                UsedCommands.Add(s);
        }

        // tokenparser
        private void addUseOf(optionAttribute attr)
        {
            addToUsedCommands(attr.command);
            foreach (string cmd in attr.alternatives)
                addToUsedCommands(cmd);
        }

        // tokenparser
        private void findChoiceAndParamToken(List<CmdLineToken> tokens, string command, string[] alternatives, out CmdLineToken choiceToken, out CmdLineToken paramToken)
        {
            IEnumerable<CmdLineToken> TokenList = from s in tokens
                                                  where (s.isChoice && (s.nonSlashedText.Equals(command) || (alternatives != null && alternatives.Contains(s.nonSlashedText))))
                                                  select s;
            // nastaveni tokenu choice a param podle nalezu v seznamu tokenu
            switch (TokenList.Count())
            {
                case 0:
                    choiceToken = null;
                    paramToken = null;
                    break;
                case 1:
                    choiceToken = TokenList.Single();
                    int i = tokens.IndexOf(choiceToken);
                    if (i < tokens.Count() - 1)
                        paramToken = tokens[i + 1];
                    else
                        paramToken = null;

                    break;
                default: throw new ParserException("Multiple use of choice {0}", TokenList.First().nonSlashedText);
            }
        }

        // tokenparser
        private void getOrdinaryParameters(List<CmdLineToken> tokens)
        {
            // seznam parametru pro program
            var zbyleTokens = from t in tokens
                              where (!t.isUsed)
                              select t;
            this._ordinaryParameters = (from t in zbyleTokens select t.originalText).ToArray();
        }

        // tokenparser
        private void checkPendingChoices(List<CmdLineToken> tokens)
        {
            // seznam nepouzitych choices -> nebyly vubec definovany -> chyba
            var chybneChoices = from t in tokens
                                where t.isChoice && !t.isUsed
                                select t;

            if (chybneChoices.Count() > 0)
                throw new ParserException("Unknown choice: {0}", chybneChoices.First().nonSlashedText);
        }

        public void parse(object args, string[] commands)
        {
            // vytvori se seznam tokenu command liny + aditivni informace
            List<CmdLineToken> tokens = createTokenList(commands);

            var optionedFields = getOptionedFields(args.GetType().GetFields());

            UsedCommands.Clear();

            // zde se dela hlavni parsovani - dane field se vyplni prislusnym typem
            foreach (var pair in optionedFields)
            {
                FieldInfo field = pair.Key;
                optionAttribute option = pair.Value;
                option.field = field;

                // kompletni kontrola chyb (typy, definice command atp.)
                option.check();

                // kontrola duplicit a zaznam o pouziti dane optiony
                addUseOf(option);

                CmdLineToken choiceToken;
                CmdLineToken paramToken;
                findChoiceAndParamToken(tokens, option.command, option.alternatives, out choiceToken, out paramToken);

                // tato metoda vyplni field podle hodnot choice a param tokenu
                option.parse(args, choiceToken, paramToken);

            }

            // ted projit zbyvajici tokeny
            getOrdinaryParameters(tokens);
            checkPendingChoices(tokens);

        }


        private static optionAttribute getOptionAttribute(FieldInfo field)
        {
            IEnumerable<optionAttribute> OptionedFields = from pro in field.GetCustomAttributes(false)
                                                         where pro is optionAttribute
                                                         select (optionAttribute)pro;

            switch (OptionedFields.Count())
            {
                case 0: return null;
                case 1: return OptionedFields.Single();
                default: throw new ParserException(string.Format("Too many options at field {0}, only one per field allowed.", field.Name));
            }
        }

        private string removeBeginSlash(string s)
        {
            if (s.Equals(ParserConsts.ARGUMENT_SEPARATOR))
                return s;

            if (s.StartsWith(ParserConsts.CHOICESTARTLONG))
                return s.Substring(ParserConsts.CHOICESTARTLONG.Length);

            if (s.StartsWith(ParserConsts.CHOICESTARTSHORT))
                return s.Substring(ParserConsts.CHOICESTARTSHORT.Length);

            return s;
        }

    }

// #pragma warning disable 649 // at mi to furt nehlasi ze ty hodnoty nepouzivam
    class arguments
    {
        /*
        // bezparametricke varianty
        [optionNoParam(command = "req_np", description="non-parametric required option", alternatives=new string[] {"t1", "t2"})]
        public bool npr;

        [optionNoParam(command = "opt_np", description = "non-parametric optional option", choiceRequired=false)]
        public bool npo;

        // intove varianty

        [optionInt(command="irr", description="Both required")]
        public int intreqreq;

        [optionInt(command="iro", description = "Only choice required", paramRequired=false, defaultValue=1)]
        public int intreqopt;

        [optionInt(command="ior", description = "Only param required", choiceRequired=false)]
        public int? intoptreq;

        [optionInt(command = "ioo", description = "Both optioonal", choiceRequired = false, 
            paramRequired=false, defaultValue=4)]
        public int? intoptopt;

        // boolove varianty
        [optionBool(command = "brr", description = "Both required-bool")]
        public bool boolreqreq;

        [optionBool(command = "bro", description = "Only choice required", paramRequired = false, defaultValue = true)]
        public bool boolreqopt;

        [optionBool(command = "bor", description = "Only param required", choiceRequired = false)]
        public bool? booloptreq;

        [optionBool(command = "boo", description = "Both optional", choiceRequired = false, paramRequired = false, defaultValue = true, alternatives = new string[] {"u1", "u2", "u3"})]
        public bool? booloptopt;

        // enumove varianty
        [optionEnum(command = "err", description = "Both req - enum", values = new string[] { "one", "two", "three" })]
        public string enumreqreq;

        [optionEnum(command = "ero", description = "Only choice required", values = new string[] { "one", "two", "three" }, paramRequired=false, defaultValue="one")]
        public string enumreqopt;

        [optionEnum(command = "eor", description = "Only param required", values = new string[] { "one", "two", "three" }, choiceRequired=false)]
        public string enumoptreq;

        [optionEnum(command = "eoo", description = "Nothing required", values = new string[] { "one", "two", "three" }, paramRequired = false, choiceRequired=false, defaultValue = "1")]
        public string enumoptopt;

        // stringove varianty
        [optionString(command = "srr", description = "Both req - string", alternatives=new string[] {"xeoo"})]
        public string strreqreq;

        [optionString(command = "sro", description = "Only choice required", paramRequired = false, defaultValue = "one")]
        public string strreqopt;

        [optionString(command = "sor", description = "Only param required", choiceRequired = false)]
        public string stroptreq;

        [optionString(command = "soo", description = "Nothing required", paramRequired = false, choiceRequired = false, defaultValue = "one")]
        public string stroptopt;

        [optionBool(command = "s")]
        public bool useStable;

    */
        [optionEnum(command = "t", paramRequired=false, defaultValue="2", values=new string[] {"1", "2", "3"})]
        public string test;


        [optionInt(command = "v")]
        public int ii;

        [optionNoParam(command = "b", alternatives=new string[] {"c"})]
        public bool bb;
    }


    class Program
    {
        static void Main(string[] args_)
        {
            string[] myArgs = new string[] { "-v", "-5", "-b", "-t", @"1"};

            arguments args = new arguments();
            Parser parser = new Parser();

            try
            {
                parser.parse(args, myArgs);
                // arguments a = parser.parse<arguments>(myArgs);
            }
            catch (ParserException e)
            {
                Console.WriteLine("ERROR: {0}", e.Message);
                Parser.printUsage(args);
                // parser.printUsage(args);
                return;
            }

            Parser.printUsage(args);
            //Console.WriteLine("Ordinary params:");
            //foreach (string param in parser.ordinaryParameters)
            //    Console.WriteLine(" {0}", param);
            Console.WriteLine();
            //parser.printUsage(args);
        }
    }
}
